<?php
/**
 * Класс для работы с древовидными структурами
 * @author chashin evgeniy
 *
 */
class Zen_Tree extends Zen_Model{

	/**
	 * Экземпляр класса Zend_Db в котором храниться структура
	 * @var Zend_Db
	 */
	protected $table;
	private $types = false;
	private $id = '`id`'; // Поле ID
	private $left = '`left`'; // Поле левого ключа
	private $right = '`right`'; // Поле правого ключа
	private $level = '`level`'; // Поле уровня
	private $parent = '`parent_id`';

	protected  $grid=false;

	private $_uniqueNeighboarFields = false;


	/**
	 * @return the $table
	 */
	public function getTable() {
		return $this->table;
	}

	/**
	 * @return the $grid
	 */
	public function getGrid() {
		return $this->grid;
	}

	/**
	 * @param field_type $grid
	 */
	public function setGrid($grid) {
		$this->grid = $grid;
	}

	/**
	 * @return the $types
	 */
	public function getTypes() {
		return $this->types;
	}

	/**
	 * @param Zend_Db $table
	 */
	public function setTable($table) {
		$this->table = $table;
		return $this;
	}

	public function getUniqueNeighboarFields() {
		return $this->_uniqueNeighboarFields;
	}

	/**
	 * @param field_type $grid
	 */
	public function setUniqueNeighboarFields($value) {
		$this->_uniqueNeighboarFields = $value;
	}
	
	
	public function getModel($type){
		$types=$this->getTypes();
		if(isset($types[$type]['model'])){
			return $types[$type]['model'];
		}else{
			throw new Zend_Exception("Doesn't exist type ".$type);
		}
	}


	/**
	 * @param field_type $types
	 */
	public function setTypes($types) {
		foreach ( $types as $k => $type ) {
			if ($k != "root") {

				if (! isset ( $type ['view'] )) {
					$types [$k] ['view'] = 'form';
				} else {
					$types [$k] ['view'] = preg_replace ( '/\..*$/', '', $type ['view'] );
				}
				if (! isset ( $type ['model'] )) {
					if (! isset ( $type ['table'] )) {
						throw new Zend_Exception ( 'Doesnt set table for type ' . $type );
					} else {
						$types [$k] ['model'] = new Zen_Item ( $type ['table'] );
					}
				}
				if ((! isset ( $type ['model'] ) && (! isset ( $type ['table'] )))) {
					throw new Zend_Exception ( "Doesn't set table and model for type " . $type );
				}

			}
		}
		$this->types = $types;
		return $this;
	}

	public function getTableName() {
		$table = $this->table;
		$info = $table->info ();
		return ($info ['name']);
	}

	protected $fields = array ('id', 'parent_id', 'position', 'left', 'right', 'level', 'label', 'nid', 'type' );

	/**
	 * Создание нового узла дерева
	 * @param int $parent_id  id родительского элемента, для корневого узла parent_id=0
	 * @param int $position позиция в списке,первый элемент имеет position=0
	 * @param array $params - параметры узла,например имя
	 * @return true|false
	 */
	public function create($params) {
		$params=$this->beforeCreate($params);
		$this->checkParams ( $params, array ('parent_id', 'type', 'label' ) );
		$parent_id = $params ['parent_id'];
		//Проверка, что $parent_id является целым числом
		$parent_id = trim ( preg_replace ( '/\D/ui', '', $parent_id ) );
		$params ['parent_id'] = $parent_id;

		if ($parent_id == "") {
			$parent_id = 0;
		}
		//Проверка, что $parent_id является целым числом
		if ($parent_id == 0) {
			$info = $this->getInfo ();
			$right_key = $info ['right'] + 1;
			$level = 0;
		} else {
			$node = $this->read ( $parent_id );
			if (! $node) {
				return false;
			}
			$right_key = $node ['right'];
			$level = $node ['level'];
		}

		$type = $params ['type'];

		$model = $this->types [$type] ['model'];
		$cid=false;
		try{
			$cid = $model->create ( $params );
		}catch(Exeption $e){
			throw $e;
		}
		if ($cid) {
			$params ['id'] = $cid;
		} else {
			throw new Exception ( "Can't create item" );
		}

		$data = array ('right' => new Zend_Db_Expr ( '`right`+2' ), 'left' => new Zend_Db_Expr ( "IF(`left` > '" . $right_key . "',`left` + 2,`left`)" ) );
		$res = $this->table->update ( $data, "`right` >= '" . $right_key . "'" );
		$params ['left'] = $right_key;
		$params ['right'] = $right_key + 1;
		$params ['level'] = $level + 1;
		$params ['parent_id'] = $parent_id;
		$db = $this->getTable ()->getAdapter ();

		$id = $this->table->createRow ( $params )->save ();
		if ($id) {
			$this->afterCreate($id, $params);
			$this->afterChange();
			$this->globalChange();
			return $id;
		} else {
			throw new Zend_Exception ( 'Cant create item in Zen_Tree' );
		}

	}

	/*
	 * Получение всей ветки, в которой учавствует наш узел
	*/
	public function getBranch($where = false, $attributes = '*') {
		if ($where == false) {
			return false;
		}

		$node = $this->getTable ()->select ()->from ( $this->getTableName (), array ('id', 'left', 'right' ) )->where ( $where )->order ( 'left DESC' )->limit ( 1 )->query ()->fetchAll ();
		if (! isset ( $node [0] ['id'] )) {
			return false;
		} else {
			$node = $node [0];
		}

		if ($attributes != '*') {
			foreach ( $attributes as $attribute ) {
			}
		} else {
			$select = $this->getTable ()->select ()->where ( '`right`>?', $node ['left'] )->where ( '`left`<?', $node ['right'] )->order ( 'left' );
		}

		$res = $select->query ()->fetchAll ();
		return $res;
	}

	private function getInfo() {
		$data = $this->table->select ()->from ( $this->table, array ('COUNT(`id`)', 'MIN(`left`)', 'MAX(`right`)' ) )->query ()->fetchAll ();
		$res = array ();
		$res ['count'] = $data [0] ['COUNT(`id`)'];
		$res ['left'] = $data [0] ['MIN(`left`)'];
		$res ['right'] = $data [0] ['MAX(`right`)'];
		if ($res ['left'] == "") {
			$res ['left'] = 0;
		}
		if ($res ['right'] == "") {
			$res ['right'] = 0;
		}
		return $res;
	}

	/**
	 * Чтение параметров одного узла дерева
	 * @param id $id
	 * @return array|false
	 */
	public function read($id) {
		$row = $this->table->find ( $id )->current ();
		if ($row) {
			return $row->toArray ();
		} else {
			return false;
		}
	}

	/**
	 * Обновление параметров одного узла
	 * @param int $id
	 * @param int $params
	 * @return bool
	 */
	public function update($id, $params) {
		$params=$this->beforeUpdate($id, $params);
		$id  = preg_replace('/\D/','',$id);
		$res = false;
		$row = $this->table->find($id)->current();
		if($row){
			$types = $this->getTypes ();
			$ItemModel = $types[$row->type] ['model'];
			$res = $ItemModel->update($id,$params);
			if ($res) {
				$res = $row->setFromArray ( $params )->save ();
			}
			if ($res) {
				$res = true;
				$this->afterUpdate($id, $params);
				$this->afterChange();
				$this->globalChange();
			}
		} else {
			throw new Zend_Exception ( 'Cant find row with id=' . $id );
		}
		return $res;
	}

	/**
	 * Удаление узла дерева и всех его потомков если они есть
	 * @param int $id
	 * @return bool
	 */
	public function delete($id) {
		$this->beforeDelete($id);
		$db = $this->getTable ()->getAdapter ();
		$db->beginTransaction ();
		try {
			$node = $this->table->find ( $id )->toArray ();
			if (! $node) {
				return false;
			}
			$node = $node [0];

			$left_key = $node ['left'];
			$right_key = $node ['right'];
			$parent_id = $node ['parent_id'];
			$deleted_nodes = $this->table->select ()->from ( $this->table, array ('id', 'type' ) )->where ( "`left` >= " . $left_key . " AND `right`<=" . $right_key )->query ()->fetchAll ();

			$status = $this->table->delete ( "`left` >= " . $left_key . " AND `right`<=" . $right_key );
			if ($status) {
				$step = $right_key - $left_key + 1;
				$data = array ('left' => new Zend_Db_Expr ( "IF(`left` > " . $left_key . ", `left`-" . $step . ", `left`)" ), 'right' => new Zend_Db_Expr ( "`right`-'" . $step . "'" ) );

				$res = $this->table->update ( $data, "`right`>" . $right_key );
			}
			if ($status) {
				$status = 1;
				if ($deleted_nodes) {
					$nodes = array ();
					foreach ( $deleted_nodes as $node ) {
						$this->types [$node ['type']] ['model']->delete ( $node ['id'] );
					}
				}
				$db->commit ();
				$this->afterDelete($id);
				$this->afterChange();
				$this->globalChange();

				return TRUE;
			} else {
				$db->rollBack ();
				return FALSE;
			}
		}

		catch ( Exception $e ) {
			$db->rollBack ();
			echo $e->getMessage ();
		}
		return true;
	}

	private function _move_unit($left, $right, $level, $level_new, $near, $new_parent_id, $old_parent_id, $id) {

		if ($near >= $left && $near <= $right) {
			return false;
		}
		if ($left <= 0 || $right <= 0 || $level <= 0 || $level_new <= 0 || $near < 0 || $new_parent_id < 0 || $old_parent_id < 0 || $id <= 0) {
			$str = "left:" . $left . ";";
			$str .= "right:" . $right . ";";
			$str .= "level :" . $level . ";";
			$str .= "level_new :" . $level_new . ";";
			$str .= "near:" . $near . ";";
			$str .= "new_parent_id:" . $new_parent_id . ";";
			$str .= "old_parent_id:" . $old_parent_id . ";";
			$str .= "id:" . $id . ";";
			throw new Zend_Exception ( 'table integrity error,<br/> data dump:' . $str );
			return false;
		}

		$skew_tree = $right - $left + 1;
		$skew_level = $level_new - $level;
		// Перемещение вверх по дереву
		if ($right < $near) {
			$skew_edit = $near - $left + 1 - $skew_tree;
			$sql = 'UPDATE ' . $this->getTableName () . '
                SET ' . $this->left . ' = CASE WHEN ' . $this->right . '<=' . $right . ' THEN ' . $this->left . '+' . $skew_edit . ' ELSE CASE WHEN ' . $this->left . '>' . $right . ' THEN ' . $this->left . '-' . $skew_tree . ' ELSE ' . $this->left . ' END END, ' . $this->level . ' = CASE WHEN ' . $this->right . '<=' . $right . ' THEN ' . $this->level . '+' . $skew_level . ' ELSE ' . $this->level . ' END, ' . $this->right . ' = CASE WHEN ' . $this->right . '<=' . $right . ' THEN ' . $this->right . '+' . $skew_edit . ' ELSE CASE WHEN ' . $this->right . '<=' . $near . ' THEN ' . $this->right . '-' . $skew_tree . ' ELSE ' . $this->right . ' END END
                WHERE ' . $this->right . '>' . $left . ' AND ' . $this->left . '<=' . $near;

		} else {
			// Перемещение вниз по дереву
			$skew_edit = $near - $left + 1;
			$sql = 'UPDATE ' . $this->getTableName () . '
                SET ' . $this->right . ' = CASE WHEN ' . $this->left . '>=' . $left . ' THEN ' . $this->right . '+' . $skew_edit . ' ELSE CASE WHEN ' . $this->right . '<' . $left . ' THEN ' . $this->right . '+' . $skew_tree . ' ELSE ' . $this->right . ' END END, ' . $this->level . ' = CASE WHEN ' . $this->left . '>=' . $left . ' THEN ' . $this->level . '+' . $skew_level . ' ELSE ' . $this->level . ' END, ' . $this->left . ' = CASE WHEN ' . $this->left . '>=' . $left . ' THEN ' . $this->left . '+' . $skew_edit . ' ELSE CASE WHEN ' . $this->left . '>' . $near . ' THEN ' . $this->left . '+' . $skew_tree . ' ELSE ' . $this->left . ' END END
                WHERE ' . $this->right . '>' . $near . ' AND ' . $this->left . '<' . $right;

		}
		$db = $this->getTable ()->getAdapter ();
		$db->beginTransaction ();
		try {
			$db->query ( $sql );
			if ($new_parent_id != $old_parent_id) {
				$this->table->update ( array ('parent_id' => $new_parent_id ), "`id`='" . $id . "'" );
			}
			$db->commit ();
			return true;
		} catch ( Exception $e ) {
			$db->rollBack ();
			return false;
		}

	}

	/**
	 * Перемещение узла дерева и всех его потомков
	 * @param int $id - id перемещаемого узла
	 * @param int $objectiveNodeId - id near узла
	 * @param str $position - позиция по отношению к near узлу
	 * @param int $parent_id - id узла, который будет родительским
	 * @param int $pos - новая позиция в списке
	 * @return bool
	 */
	public function reorder($id, $objectiveNodeId, $position, $pos, $parent_id) {

		/*
		 * Проверка параметров
		*/

		$res = false;
		
		$result = $this->_beforeReorder($id,$parent_id);
		if (!$result) return false;
		
		//Проверка, что $id является целым числом
		$id = trim ( preg_replace ( '/\D/ui', '', $id ) );
		//Проверка, что $parent_id является целым числом
		$objectiveNodeId = trim ( preg_replace ( '/\D/ui', '', $objectiveNodeId ) );
		$parent_id = trim ( preg_replace ( '/\D/ui', '', $parent_id ) );
		$pos = trim ( preg_replace ( '/\D/ui', '', $pos ) );

		if (($id == "") || ($objectiveNodeId == "")) {
			return false;
		}

		$node = $this->read ( $id );
		if (! isset ( $node ['left'] )) {
			return false;
		}

		//Если сменен parent_id
		if ($node ['parent_id'] != $parent_id) {
			//Если в подчинение другому узлу
			if ($parent_id > 0) {
				$newParent = $this->read ( $parent_id );
				$level_new = $newParent ['level'] + 1;
				if ($position == 'first') {
					$newParent = $this->read ( $parent_id );
					if (! $newParent) {
						return FALSE;
					}
					$near = $newParent ['left'] - 1;
				}
				if ($position == 'last') {
					$newParent = $this->read ( $parent_id );
					if (! $newParent) {
						return FALSE;
					}
					$near = $newParent ['right'] - 1;
				}
				if ($position == 'before') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['left'] - 1;
				}
				if ($position == 'after') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['right'];
				}
			}
			//Если в корень
			if ($parent_id == 0) {
				$level_new = 1;
				if ($position == 'first') {
					$near = 1;
				}
				if ($position == 'last') {
					$info = $this->getInfo ();
					$near = $info ['right'];
				}
				if ($position == 'before') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['left'] - 1;
				}
				if ($position == 'after') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['right'];
				}
			}
		}

		//Если не сменен parent_id
		if ($node ['parent_id'] == $parent_id) {
			//Если перемещение в корне
			if ($parent_id == 0) {
				$level_new = 1;

				if ($position == 'first') {
					$near = 0;
				}

				if ($position == 'last') {
					$info = $this->getInfo ();
					$near = $info ['right'];
				}
				if ($position == 'before') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['left'] - 1;
				}
				if ($position == 'after') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['right'];
				}
			} else {
				$level_new = $node ['level'];
				if ($position == 'first') {
					$newParent = $this->read ( $parent_id );
					if (! $newParent) {
						return FALSE;
					}
					$near = $newParent ['left'] - 1;
				}
				if ($position == 'last') {
					$newParent = $this->read ( $parent_id );
					if (! $newParent) {
						return FALSE;
					}
					$near = $newParent ['right'] - 1;
				}

				if ($position == 'before') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['left'] - 1;
				}
				if ($position == 'after') {
					$obj = $this->read ( $objectiveNodeId );
					if (! $obj) {
						return FALSE;
					}
					$near = $obj ['right'];
				}
			}
		}

		$res = $this->_move_unit ( $node ['left'], $node ['right'], $node ['level'], $level_new, $near, $parent_id, $node ['parent_id'], $node ['id'] );
		$this->afterReorder($id);
		$this->afterChange();

		return $res;
	}

	public function select() {
		$select = new Zen_Tree_Select ();
		$select->setTable ( $this->getTable () );
		$types = $this->getTypes ();
		$selectTypes = array ();
		foreach ( $types as $type => $params ) {
			if (isset ( $params ['table'] )) {
				$selectTypes [$type] = $params ['table'];
			}
		}

		$select->setTypes ( $selectTypes );
		return $select;
	}

	protected function checkParams($params, $mandatory) {
		$deny = array ();
		foreach ( $mandatory as $field ) {
			if (! isset ( $params [$field] )) {
				$deny [] = $field;
			}
		}
		if (sizeof ( $deny )) {
			throw new Zend_Exception ( 'Doesnt set params: [' . implode ( ',', $deny ) . ']' );
		}
	}

	//EVENTS
	public function beforeCreate($params) {
		$params = $this->raiseBehavior('beforeCreate', $params);
		return $params;
	}
	public function afterCreate($id, $params) {
		$params['id'] = $id;
		$params = $this->raiseBehavior('afterCreate', $params);
		unset($params['id']);
		return $params;
	}

	public function afterRead($params) {
		$params = $this->raiseBehavior('afterRead', $params);
		return $params;
	}
	public function beforeDelete($id) {
		$this->raiseBehavior('beforeDelete', array('id'=>$id));
	}
	public function afterDelete($id) {
		$this->raiseBehavior('afterDelete', array('id'=>$id));
	}

	public function beforeUpdate($id, $params) {
		$params['id'] = $id;
		$params = $this->raiseBehavior('beforeUpdate', $params);
		unset($params['id']);
		return $params;
	}

	public function afterUpdate($id, $params) {
		$params['id'] = $id;
		$params = $this->raiseBehavior('afterUpdate', $params);
		unset($params['id']);
		return $params;
	}
	private function _beforeReorder($id,$parent_id) {
		//Проверка уникальности полей при перемещении ветки
		
		$uniqueNeighboarFields = $this->getUniqueNeighboarFields();
		if ( $uniqueNeighboarFields && is_array($uniqueNeighboarFields)) {
			if ( count($uniqueNeighboarFields)>0 ) {
				foreach ($uniqueNeighboarFields as $type=>$fields) {
					foreach ($fields as $field){
						$this_item = $this
		        				->select()
		        				->setColumns(array('tree'=>array('parent_id'),$type=>array($field)))
		        				->getOne("tree.id=$id");
		        		if (! $this_item) throw new Exception('Error! There is no item given.');
						
						$res = $this
		        				->select()
		        				->setParentId($parent_id)
		        				->setColumns(array($type=>array('id',$field)))
		        				->getData("$field='" . $this_item[$field] . "'");
			        	if ( count($res) > 1 ) return false;
			        	if ( count($res)==1 && $res[0]['id'] != $id )  return false;
					}
				}
			}
		}
		$res = $this->beforeReorder($id, $parent_id);
		return $res;
	}
	
	public function beforeReorder($id,$parent_id) {
		$this->raiseBehavior('beforeReorder', array('id'=>$id,'parent_id'=>$parent_id));
		return true;
	}
	
	public function afterReorder($id) {
		$this->raiseBehavior('afterReorder', array('id'=>$id));
	}

	public function afterChange() {
		$this->raiseBehavior('afterChange');
	}
	//Для глобальных изменений
	public function globalChange() {
		//Чистка кэша
		Cache_Model_Clear::clear ( 'pages' );
		Cache_Model_Clear::clear ( 'block' );
		Cache_Model_Clear::clear ( 'search' );
	}

}